import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
import { defer, map, concatMap, timeout, catchError, of, Observable, tap, } from 'rxjs';
import axios, { RawAxiosRequestHeaders } from 'axios';
import IHelper from '@/services/helper.service';
import ILogger from '@/services/logger.service';
import { IWonewSer } from '@/typing';
import IResponse from '@/services/response.service';
import * as fs from "fs-extra"
import * as path from "path"
import * as moment from "moment"
import bili_typelistJSon from "./json/bili_typelist.json"
import { PubTaskEntity } from '@/db/entitys/pub-task.entity';
import { AccountEntity } from '@/db/entitys/account.entity';
import { VideoEntity } from '@/db/entitys/video.entity';
import { ConfigService } from '@nestjs/config';
import { error } from 'console';
import { BiliBindService } from './bili.bind.service';
import * as puppeteer from 'puppeteer';
import IConstants from '@/core/constants';
import * as shelljs from "shelljs"

@Injectable()
export class BiliService {
  constructor(
    private readonly biliBindService: BiliBindService
  ) {}
  private readonly UPLOAD_PROFILE: string = "ugcupos/bup"

  publishVideoToAccount(taskSchema: PubTaskEntity, accountSchema: AccountEntity, videoSchema: VideoEntity): Observable<IWonewSer.PublishTaskResult> {
    return defer(() => this.publishVideoToAccountByPuppeteer(taskSchema, accountSchema, videoSchema))
  }

  async publishVideoToAccountByPuppeteer(taskSchema: PubTaskEntity, accountSchema: AccountEntity, videoSchema: VideoEntity): Promise<IWonewSer.PublishTaskResult> {
    let browser: puppeteer.Browser | undefined = undefined
    let res: IWonewSer.PublishTaskResult = {status: false, message: "未知错误", resp: null}
    try {
      browser = await puppeteer.launch({
        headless: false,
        defaultViewport: {width: 1680, height: 900},
        args: ['--no-sandbox','--disable-setuid-sandbox', "--window-size=1680,900", "--disable-notifications", ],
      })
  
      const page = await browser.newPage()
      const cookies = JSON.parse(accountSchema.cookie) as puppeteer.Cookie[]
      for(let i = 0; i < cookies.length; i++){
        await page.setCookie(cookies[i])
      }
      await page.goto("https://member.bilibili.com/platform/upload/video/frame")
      await page.evaluate(() => {
        localStorage.setItem("bili_videoup_notification_authority", "denied")
      })
      const videoPath = path.join(IConstants.publicDir, `uploads/temp/${taskSchema.title}.mp4`)
      fs.copyFileSync(IHelper.getAssetAbsPathByStaticPath(videoSchema.video), videoPath)
      const inputEle = await page.waitForSelector(".bcc-upload.upload input")
      await inputEle.uploadFile(videoPath)
      await page.waitForSelector(".file-block-icon-success")
      await page.waitForSelector(".hot-tag-container")
      
      const topicList = await page.$$(".tag-topic-list .hot-tag-item")
      const randomTopic = topicList.random()
      if (randomTopic) {
        await randomTopic.click()
      }
  
      const tagInput = await page.$(".tag-input-wrp .input-val")
      const taskTags = taskSchema.common_tag.split(",").filter(e => e)
      for (let i = 0; i < taskTags.length; i++) {
        await tagInput.type(taskTags[i])
        await page.keyboard.press("Enter")
        await IHelper.sleep(200)
      }
      const tagList = await page.$$(".tag-list .hot-tag-item")
      for(let i = 0; i < 9; i++) {
        let randomTag = tagList.random()
        if (randomTag) {
          await randomTag.click()
        }
      }
      await IHelper.sleep(5 * 1000)
      const submitBtn = await page.waitForSelector(".submit-add")
      await submitBtn.click()
      const response = await page.waitForResponse(response => {
        return response.url().includes("https://member.bilibili.com/x/vu/web/add/v")
      })
      const result = await response.json()
      if (!result?.data?.bvid) {
        throw new Error(result.message || "Result 错误")
      }
      ILogger.info(`发布[${videoSchema.name}]到账号[${accountSchema.name}]: ${JSON.stringify(result)}`)
      res = {status: false, message: "success", resp: result}
    } catch(error) {
      res.message = error.message || "未知错误"
    }
    if (browser) {
      browser.close().catch(e => e)
    }
    return res
  }

  
  publishVideoToAccount1(taskSchema: PubTaskEntity, accountSchema: AccountEntity, videoSchema: VideoEntity): Observable<IWonewSer.PublishTaskResult> {
    const task: IWonewSer.BiliPublishTask = {
      uuid: `发布[${videoSchema.name}]到账号[${accountSchema.name}]`,
      startAt: Date.now(),
      taskSchema, accountSchema, videoSchema, 
      videoBuffer: fs.readFileSync(IHelper.getAssetAbsPathByStaticPath(videoSchema.video)),
      coverBuffer: fs.readFileSync(IHelper.getAssetAbsPathByStaticPath(videoSchema.cover)),
    }
    // @ts-ignore
    return this.biliBindService.checkCookie(accountSchema.cookie).pipe(
      map(res => {
        if (!res.valid) {
          ILogger.info(`Bili pub error: Bili cookie expired.`)
          throw new HttpException("Bili cookie expired.", HttpStatus.BAD_REQUEST)
        }
        return task
      }),
      tap(() => ILogger.info(`${task.uuid} start uploadVideo_preUpload`)),
      concatMap(() => this.uploadVideo_preUpload(task)),
      tap(() => ILogger.info(`${task.uuid} start uploadVideo_getUploadId`)),
      concatMap(() => this.uploadVideo_getUploadId(task)),
      tap(() => ILogger.info(`${task.uuid} start uploadVideo_uploadPreChuck`)),
      concatMap(() => this.uploadVideo_uploadPreChuck(task)),
      tap(() => ILogger.info(`${task.uuid} start uploadVideo_checkStatus`)),
      concatMap(() => this.uploadVideo_checkStatus(task)),
      tap(() => ILogger.info(`${task.uuid} start uploadCover`)),
      concatMap(() => this.uploadCover(task)),
      tap(() => ILogger.info(`${task.uuid} start publishVideo`)),
      concatMap(() => this.publishVideo(task)),
      tap(() => ILogger.info(`${task.uuid} end publishVideo`)),
      map(() => {
        const {videoBuffer, coverBuffer, ...rest} = task
        return {status: true, message: "success", resp: rest}
      }),
      catchError(error => {
        const {videoBuffer, coverBuffer, ...rest} = task
        return of({status: false, message: error.message, resp: rest})
      })
    )
  }

  private uploadVideo_preUpload(task: IWonewSer.BiliPublishTask) {
    const params = {
      'probe_version': '20221109',
      'upcdn': 'bldsa',
      'zone': 'cs',
      'name': path.basename(task.videoSchema.video),
      'r': 'upos',
      'profile': this.UPLOAD_PROFILE,
      'ssl': '0',
      'version': '2.14.0.0',
      'build': '2140000',
      'size': task.videoBuffer.length.toString(),
      'webVersion': '2.14.0'
    }
    const uri: string = `https://member.bilibili.com/preupload?${IHelper.convertToQueryString(params)}`
    const headers: RawAxiosRequestHeaders = {
      'Host': 'member.bilibili.com',
      'Cookie': IHelper.covert2StandardCookieStr(task.accountSchema.cookie),
      'cache-control': 'max-age=0',
      'sec-ch-ua': '"Not_A Brand";v="8", "Chromium";v="120", "Google Chrome";v="120"',
      'sec-ch-ua-mobile': '?0',
      'sec-ch-ua-platform': '"macOS"',
      'upgrade-insecure-requests': '1',
      'user-agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
      'accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7',
      'sec-fetch-site': 'none',
      'sec-fetch-mode': 'navigate',
      'sec-fetch-user': '?1',
      'sec-fetch-dest': 'document',
      'accept-language': 'zh-CN,zh;q=0.9',
      'referer': 'https://www.bilibili.com/'
    }
    return defer(() =>
      axios.get(uri, { headers })
    ).pipe(
      map(res => {
        const upos_uri = res.data?.upos_uri
        if (!upos_uri) {
          ILogger.info(`Bili pub error: Get upos uri failed. ${JSON.stringify(res.data)}`)
          throw new HttpException(`Get upos uri failed.`, HttpStatus.BAD_REQUEST)
        }
        task.res_preUpload = res.data
        return task
      })
    )
  }

  private uploadVideo_getUploadId(task: IWonewSer.BiliPublishTask) {
    const upOsUri = this.getTaskUpOsUri(task)
    const upOsAuth: string = this.getTaskUpOsAuth(task)
    const uri = `https://upos-cs-upcdnbldsa.bilivideo.com/${upOsUri}?uploads&output=json`
    return defer(() =>
      axios.post(uri,'', {
          headers: {
            'Host': 'upos-cs-upcdnbldsa.bilivideo.com',
            'Pragma': 'no-cache',
            'Cache-Control': 'no-cache',
            'sec-ch-ua': '"Not_A Brand";v="8", "Chromium";v="120", "Google Chrome";v="120"',
            'X-Upos-Auth': upOsAuth,
            'sec-ch-ua-mobile': '?0',
            'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
            'sec-ch-ua-platform': '"macOS"',
            'Accept': '*/*',
            'Origin': 'https://member.bilibili.com',
            'Sec-Fetch-Site': 'cross-site',
            'Sec-Fetch-Mode': 'cors',
            'Sec-Fetch-Dest': 'empty',
            'Referer': 'https://www.bilibili.com/',
            'Accept-Language': 'zh-CN,zh;q=0.9',
            'Content-Type': 'application/x-www-form-urlencoded'
          }
        }
      )
    ).pipe(
      map(d => {
        if (!d.data?.upload_id) {
          ILogger.info(`Bili pub error: Get upload id failed. ${JSON.stringify(d.data)}`)
          throw new HttpException(`Get upload id failed.`, HttpStatus.BAD_REQUEST)
        }
        task.res_getUploadId = d.data
        return task
      }),
    )
  }

  private uploadVideo_uploadPreChuck(task: IWonewSer.BiliPublishTask) {
    const upOsUri = this.getTaskUpOsUri(task)
    const uploadId: string = this.getTaskUploadId(task)
    const upOsAuth: string = this.getTaskUpOsAuth(task)
    const uri = `https://upos-cs-upcdnbldsa.bilivideo.com/${upOsUri}`
    return defer(() =>
      axios.put(uri, task.videoBuffer.buffer, {
          params: {
            'partNumber': '1',
            'uploadId': uploadId,
            'chunk': '0',
            'chunks': '1',
            'size': task.videoBuffer.length,
            'start': '0',
            'end': task.videoBuffer.length,
            'total': task.videoBuffer.length,
          },
          headers: {
            'Host': 'upos-cs-upcdnbldsa.bilivideo.com',
            'Pragma': 'no-cache',
            'Cache-Control': 'no-cache',
            'sec-ch-ua': '"Not_A Brand";v="8", "Chromium";v="120", "Google Chrome";v="120"',
            'Content-Type': 'application/octet-stream',
            'X-Upos-Auth': upOsAuth,
            'sec-ch-ua-mobile': '?0',
            'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
            'sec-ch-ua-platform': '"macOS"',
            'Accept': '*/*',
            'Origin': 'https://member.bilibili.com',
            'Sec-Fetch-Site': 'cross-site',
            'Sec-Fetch-Mode': 'cors',
            'Sec-Fetch-Dest': 'empty',
            'Referer': 'https://www.bilibili.com/',
            'Accept-Language': 'zh-CN,zh;q=0.9'
          }
        }
      )
    ).pipe(
      map(d => {
        task.res_uploadPreChuck = d.data
        return task
      })
    )
  }

  private uploadVideo_checkStatus(task: IWonewSer.BiliPublishTask) {
    const upOsUri: string = this.getTaskUpOsUri(task)
    const uploadId: string = this.getTaskUploadId(task)
    const upOsAuth: string = this.getTaskUpOsAuth(task)
    const bizId: string = this.getTaskBizId(task)
    const uri: string = `https://upos-cs-upcdnbldsa.bilivideo.com/${upOsUri}`
    const data = {
      'parts': [
        {
          'partNumber': 1,
          'eTag': 'etag'
        }
      ]
    }
    const params = {
      'output': 'json',
      'name': path.basename(task.videoSchema.video),
      'profile': this.UPLOAD_PROFILE,
      'uploadId': uploadId,
      'biz_id': bizId
    }
    const headers: RawAxiosRequestHeaders = {
      'Host': 'upos-cs-upcdnbldsa.bilivideo.com',
      'sec-ch-ua': '"Not_A Brand";v="8", "Chromium";v="120", "Google Chrome";v="120"',
      'Content-Type': 'application/json; charset=UTF-8',
      'X-Upos-Auth': upOsAuth,
      'sec-ch-ua-mobile': '?0',
      'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
      'sec-ch-ua-platform': '"macOS"',
      'Accept': '*/*',
      'Origin': 'https://member.bilibili.com',
      'Sec-Fetch-Site': 'cross-site',
      'Sec-Fetch-Mode': 'cors',
      'Sec-Fetch-Dest': 'empty',
      'Referer': 'https://www.bilibili.com/',
      'Accept-Language': 'zh-CN,zh;q=0.9'
    }
    return defer(() =>
      axios.post(uri, data, {params, headers})
    ).pipe(
      map(d => {
        if (d.data?.OK !== 1 || !d.data?.key) {
          ILogger.info(`Bili pub error: Check upload status failed. ${JSON.stringify(d.data)}`)
          throw new HttpException(`Check upload status failed.`, HttpStatus.BAD_REQUEST)
        }
        task.res_checkStatus = d.data
        return task
      })
    )
  }

  private uploadCover(task: IWonewSer.BiliPublishTask) {
    const uri: string = 'https://member.bilibili.com/x/vu/web/cover/up'
    const data = new URLSearchParams({
      'cover': `data:image/png;base64,${task.coverBuffer.toString('base64')}`,
      'csrf': this.getTaskCsrf(task)
    })
    const params = {
      't': Date.now().toString()
    }
    const headers: RawAxiosRequestHeaders = {
      'Host': 'member.bilibili.com',
      'Cookie': IHelper.covert2StandardCookieStr(task.accountSchema.cookie),
      'pragma': 'no-cache',
      'cache-control': 'no-cache',
      'sec-ch-ua': '"Not_A Brand";v="8", "Chromium";v="120", "Google Chrome";v="120"',
      'accept': 'application/json, text/plain, */*',
      'sec-ch-ua-mobile': '?0',
      'user-agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
      'sec-ch-ua-platform': '"macOS"',
      'origin': 'https://member.bilibili.com',
      'sec-fetch-site': 'same-origin',
      'sec-fetch-mode': 'cors',
      'sec-fetch-dest': 'empty',
      'referer': 'https://www.bilibili.com/',
      'accept-language': 'zh-CN,zh;q=0.9'
    }
    return defer(() =>
      axios.post(uri, data, {params, headers})
    ).pipe(
      map(res => {
        console.log("res", res.data)
        if (!res.data?.data?.url) {
          ILogger.info(`Bili pub error: Upload cover failed. ${JSON.stringify(res.data)}`)
          throw new HttpException(`Upload cover failed.`, HttpStatus.BAD_REQUEST)
        }
        task.res_uploadCover = res.data
        return task
      })
    )
  }

  getTaskUpOsUri(task: IWonewSer.BiliPublishTask): string {
    return task.res_preUpload.upos_uri.replace("upos://", "")
  }

  getTaskUploadId(task: IWonewSer.BiliPublishTask): string {
    return task.res_getUploadId.upload_id
  }

  getTaskUpOsAuth(task: IWonewSer.BiliPublishTask): string {
    return task.res_preUpload.auth
  }

  getTaskBizId(task: IWonewSer.BiliPublishTask): string {
    return task.res_preUpload.biz_id
  }

  getTaskCsrf(task: IWonewSer.BiliPublishTask) {
    const cookies: puppeteer.Protocol.Network.Cookie[] = JSON.parse(task.accountSchema.cookie)
    return cookies.find(e => e.name === "bili_jct")?.value || ""
  }

  private publishVideo(task: IWonewSer.BiliPublishTask) {
    const uri: string = 'https://member.bilibili.com/x/vu/web/add/v3'
    const upOsUri: string = this.getTaskUpOsUri(task)
    const bizId: string = this.getTaskBizId(task)
    const postData = {
      cover: task.res_uploadCover.data.url,
      title: task.taskSchema.title,
      copyright: 1,
      tid: +task.taskSchema.bili_type.split("_").pop(),
      tag: task.taskSchema.common_tag,
      'desc_format_id': 0,
      'desc': '',
      'recreate': -1,
      'dynamic': '',
      'interactive': 0,
      videos: [
        {
          'filename': upOsUri.split("/").pop().split(".")[0],
          'title': path.basename(task.videoSchema.video),
          'desc': '',
          'cid': bizId
        }
      ],
      'act_reserve_create': 0,
      'no_disturbance': 0,
      'no_reprint': 1,
      'subtitle': {
        'open': 0,
        'lan': ''
      },
      'dolby': 0,
      'lossless_music': 0,
      'up_selection_reply': false,
      'up_close_reply': false,
      'up_close_danmu': false,
      'web_os': 2,
      'csrf': this.getTaskCsrf(task)
    }
    const params = {
      't': Date.now().toString(),
      'csrf': this.getTaskCsrf(task)
    }
    const headers: RawAxiosRequestHeaders = {
      'Host': 'member.bilibili.com',
      'Cookie': IHelper.covert2StandardCookieStr(task.accountSchema.cookie),
      'pragma': 'no-cache',
      'cache-control': 'no-cache',
      'sec-ch-ua': '"Not_A Brand";v="8", "Chromium";v="120", "Google Chrome";v="120"',
      'accept': 'application/json, text/plain, */*',
      'content-type': 'application/json;charset=UTF-8',
      'sec-ch-ua-mobile': '?0',
      'user-agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
      'sec-ch-ua-platform': '"macOS"',
      'origin': 'https://member.bilibili.com',
      'sec-fetch-site': 'same-origin',
      'sec-fetch-mode': 'cors',
      'sec-fetch-dest': 'empty',
      'referer': 'https://www.bilibili.com/',
      'accept-language': 'zh-CN,zh;q=0.9'
    }
    return defer(() =>
      axios.post(uri, postData,{params,headers})
    ).pipe(
      map(res => {
        if (!res.data?.data?.bvid) {
          ILogger.info(`Bili pub error: Publish video failed. ${JSON.stringify(res.data)}`)
          throw new HttpException(`Publish video failed: ${res.data?.message}`, HttpStatus.BAD_REQUEST)
        }
        ILogger.info(`Bili pub success ${JSON.stringify(res.data)}`)
        task.res_publishVideo = res.data
        return task
      })
    )
  }

  getTypeList() {
    const uri: string = `https://member.bilibili.com/x/vupre/web/archive/pre`
    const params = {
      'lang': 'cn',
      't': Date.now().toString()
    }
    const headers: RawAxiosRequestHeaders = {
      'authority': 'member.bilibili.com',
      'accept': 'application/json, text/plain, */*',
      'accept-language': 'zh-CN,zh;q=0.9',
      'cache-control': 'no-cache',
      'cookie': "buvid_fp=54d4d2688255f83f5c2bed3866603675; DedeUserID=404186078; bili_jct=c783c6c5351736e28e017843a93ba871; SESSDATA=973b7253%2C1721469603%2Cd14d7%2A11CjDWJxGNsfe7Q290BOV_bCJMEQeyOS0AHitVtTcyO5AYxbqgfUh2iKEihuB-q_DUm3gSVm4yQmQ1WndKdlF5dFRrVUQxZGZPT1pZaHp2ZV84WXpXeGJzQ2pPdUJaVHYzb0JuX0wxOEV3MTQtRllhcXB1OFVvWU80R2VnaUdqRF9aY2F5d1NudUJnIIEC; sid=nsklzvf2; bili_ticket_expires=1706176737; _uuid=A48162103-F27D-5D31-7166-61EF8FCEB4D1096627infoc; buvid3=24C2685D-FB6A-5DCF-2955-A11B3BD37A5F96874infoc; buvid4=5FE8244C-0564-A080-782C-693A3F2D218D97280-024012209-ldELT8AqVeT%2BEt2IQnT0hg%3D%3D; b_nut=1705917596; DedeUserID__ckMd5=f3b1debd6c153fcf; bili_ticket=eyJhbGciOiJIUzI1NiIsImtpZCI6InMwMyIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3MDYxNzY3OTcsImlhdCI6MTcwNTkxNzUzNywicGx0IjotMX0.e9hd_W5e_-VvJru7YnoHGupAr_PjJd-3GMfhtlib3GY; b_lsid=72E7D6E2_18D309CC3C0",
      'pragma': 'no-cache',
      'referer': 'https://www.bilibili.com/',
      'sec-ch-ua': '"Not_A Brand";v="8", "Chromium";v="120", "Google Chrome";v="120"',
      'sec-ch-ua-mobile': '?0',
      'sec-ch-ua-platform': '"macOS"',
      'sec-fetch-dest': 'empty',
      'sec-fetch-mode': 'cors',
      'sec-fetch-site': 'same-origin',
      'user-agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'
    }
    return defer(() =>
      axios.get(uri, {params,headers})
    ).pipe(
      map(d => d.data.data.typelist),
      timeout(1500),
      catchError(() => {
        return of(
          JSON.parse(fs.readFileSync(path.join(__dirname, "json/bili_typelist.json"), "utf-8"))
        )
      }),
      map(res => IResponse.success(res))
    )
  }

  bbDownload(bvid: string): Observable<string> {
    // let cmd = `BBDown ${bvid} --config-file /Users/eros/.config/BBdown/BBdown.conf`
    const workDir = path.join(IConstants.publicDir, `uploads/bili_temp`)
    fs.ensureDirSync(workDir)
    const filename = IHelper.hash(32)
    let cmd = `BBDown ${bvid} -F ${filename} -tv --work-dir ${workDir}`
    ILogger.info("BBDown cmd: " + cmd)
    let startTime = Date.now()
    ILogger.info(`开始下载视频`)
    return new Observable(obs => {
      shelljs.exec(cmd, {silent: true}, function(code: number, output) {
        const error = new HttpException(`Download one video failed.`, HttpStatus.BAD_REQUEST)
        if (code !== 0) {
          console.log(output)
          obs.error(error)
          return
        }
        // @ts-ignore
        ILogger.info(`完成下载视频--- ${moment.duration(Date.now() - startTime, "milliseconds").format("HH:mm:ss.SSS", {trim: false})}`)
        obs.next(path.join(workDir, `${filename}.mp4`))
        obs.complete()
      })
    })
  }
}